// Copyright 2018 The Go Authors. All rights reserved.
// Use of this session code is governed by a BSD-style
// license that can be found in the LICENSE file.

package session

import (
	"context"
	"errors"
	"mi/compiler/ast"
	"mi/compiler/binder"
	"mi/compiler/checker"
	"mi/compiler/token"
	"mi/internal/span"
	"mi/internal/xpath"
)

type DefinitionInfo struct {
	Uri xpath.URI
	Range span.UTF16Span
}

func (s *session) GetDefinitionAtPosition(_ context.Context, uri xpath.URI, pos span.UTF16Position) ([]DefinitionInfo, error) {
	s.programMux.Lock()
	defer s.programMux.Unlock()

	var sourceFile = s.program.GetSourceFile(uri)
	if sourceFile == nil {
		return nil, errors.New("Unknown file " + uri.Filename())
	}

	// Convert utf16 pos to byte offset
	offset, err := pos.ToOffsetWithCache(sourceFile.Text, sourceFile.GetLineStarts())
	if err != nil {
		return nil, err
	}

	var node = getTouchingName(sourceFile, offset, true)
	if node == sourceFile {
		return nil, nil
	}

	var typeChecker = s.program.GetTypeChecker()
	var symbol = typeChecker.GetSymbolAtLocation(node)

	// Could not find a symbol e.g. ast is string or number keyword,
	// or the symbol was an internal symbol and does not have a declaration e.g. undefined symbol
	if symbol == nil || symbol.Flags&binder.Package != 0 {
		return nil, nil
	}

	return getDefinitionFromSymbol(typeChecker, symbol, node), nil
}

func (s *session) GetTypeDefinitionAtPosition(_ context.Context, uri xpath.URI, pos span.UTF16Position) ([]DefinitionInfo, error) {
	s.programMux.Lock()
	defer s.programMux.Unlock()

	var sourceFile = s.program.GetSourceFile(uri)
	if sourceFile == nil {
		return nil, errors.New("Unknown file " + uri.Filename())
	}

	// Convert utf16 pos to byte offset
	offset, err := pos.ToOffsetWithCache(sourceFile.Text, sourceFile.GetLineStarts())
	if err != nil {
		return nil, err
	}

	var node = getTouchingName(sourceFile, offset, true)
	if node == sourceFile {
		return nil, nil
	}

	var typeChecker = s.program.GetTypeChecker()
	var symbol = typeChecker.GetSymbolAtLocation(node)
	if symbol == nil {
		return nil, nil
	}

	var tpe = typeChecker.GetTypeOfSymbolAtLocation(symbol, node)
	if isNull(tpe) {
		return nil, nil
	}

	if tpe.Symbol() == nil || tpe.Symbol().Flags&binder.Package != 0{
		return nil, nil
	}

	return getDefinitionFromSymbol(typeChecker, tpe.Symbol(), node), nil
}

func getDefinitionFromSymbol(typeChecker checker.TypeChecker, symbol *binder.Symbol, node ast.Node) []DefinitionInfo {
	var result []DefinitionInfo

	if isConstructCallExpression(node) || ast.IsTokenNodeAndEqualKind(node, token.NewKeyword) {
		if symbol.Flags&binder.ClassLike != 0 {
			var tpe = typeChecker.GetTypeOfSymbol(symbol)
			var signatures = typeChecker.GetSignaturesOfType(tpe, checker.SIKConstruct)
			for _, signature := range signatures {
				result = append(result, createDefinitionInfo(signature.Declaration))
			}
		}
		return result
	}

	if isCallExpressionTarget(node) {
		signature, candidates := typeChecker.GetResolvedSignature(getContainCallExpression(node))
		if (signature == nil || typeChecker.IsUnknownSignature(signature)) && len(candidates) > 0 {
			signature = candidates[0]
		}

		if signature != nil && !isNull(signature.Declaration) {
			result = append(result, createDefinitionInfo(signature.Declaration))
		}
		return result
	}
	if isDocumentCallExpressionTarget(node) {
		signature, candidates := typeChecker.GetResolvedSignature(getContainDocumentCallExpression(node))
		if (signature == nil || typeChecker.IsUnknownSignature(signature)) && len(candidates) > 0 {
			signature = candidates[0]
		}

		if signature != nil && !isNull(signature.Declaration) {
			result = append(result, createDefinitionInfo(signature.Declaration))
		}
		return result
	}

	if isNameOfFunctionDeclaration(node) {
		if symbol.Flags&binder.MethodLike != 0 {
			var tpe = typeChecker.GetTypeOfSymbol(symbol)
			var signatures = typeChecker.GetSignaturesOfType(tpe, checker.SIKCall)
			for _, signature := range signatures {
				result = append(result, createDefinitionInfo(signature.Declaration))
			}
		}
		return result
	}

	for _, declaration := range symbol.Declarations {
		result = append(result, createDefinitionInfo(declaration))
	}

	return result
}

/** Creates a DefinitionInfo from a Declaration, using the declaration's name if possible. */
func createDefinitionInfo(node ast.Node) DefinitionInfo {
	var sourceFile = ast.GetSourceFileOfNode(node)
	return DefinitionInfo{
		Uri:   sourceFile.Uri,
		Range: getNodeSpan(node),
	}
}